﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using Ignitron.EEngine;
using Ignitron.EEngine.Maths;
using Ignitron.EEngine.Model;

namespace Board3D.Models
{
    /// <summary>
    /// Mouse Picking Class Resolver
    /// </summary>
    internal static class RayTracer
    {
        /// <summary>
        /// Converts mouse coordinates to texture coordinates the mouse is pointing at
        /// </summary>
        public static Point MouseCoordinatesToTextureCoordinates(int mouseX, int mouseY, Engine engine, EModel model, EMatrix4 modelMatrix,
                        int textureWidth, int textureHeight, ref EVector3 collisionVector)
        {
            // maxtrix initialization
            EMatrix4 projectMat = engine.GetProjectionMatrix();
            EMatrix4 viewMat = engine.GetViewMatrix();
            EMatrix4 worldMatrix = engine.GetWorldMatrix();
            Size size = engine.GetViewportSize();

            modelMatrix = EMatrix4.Invert(modelMatrix);
            worldMatrix = EMatrix4.Invert(worldMatrix);

            // do unprojection
            EVector3 ray;
            EVector3 pos1 = UnProject(ref projectMat, ref viewMat, size, new EVector2((float)mouseX, (float)mouseY), out ray);
            //pos1 = Vector3d.Transform(pos1, worldMatrix);
            //ray = Vector3d.Transform(ray, worldMatrix);
            pos1 = EVector3.Transform(pos1, modelMatrix);
            ray = EVector3.Transform(ray, modelMatrix);
            ray = ray - pos1;
            //Vector3d pos2 = UnProject(ref projectMat, ref viewMat, size, new Vector2d((double)mouseX, (double)mouseY), 1);
            //pos2 = Vector3d.Transform(pos2, modelMatrix);
            //Vector3d ray = Vector3d.Normalize(pos1);

            // do ray  testing against the model
            IntersectionResult resultTest = RayModelTest(ray, pos1, model);
            if (resultTest != null)
            {
                EVector2 uv = EVector2.BaryCentric(
                        resultTest.TextureCoords[0],
                        resultTest.TextureCoords[1],
                        resultTest.TextureCoords[2],
                        resultTest.Intersection.U,
                        resultTest.Intersection.V);
                EVector3 pos = EVector3.BaryCentric(
                        resultTest.FaceVertices[0],
                        resultTest.FaceVertices[1],
                        resultTest.FaceVertices[2],
                        resultTest.Intersection.U,
                        resultTest.Intersection.V);

                collisionVector = pos;

                Point outputPoint = new Point((int)(((double)textureWidth) * uv.X), (int)(((double)textureHeight) * uv.Y));
                return outputPoint;
            }

            return new Point(-1, -1);
        }

        public static EVector3 UnProject(ref EMatrix4 projection, ref EMatrix4 view, Size viewport, EVector2 mouse, out EVector3 ray)
        {
            EVector4 vec;

            vec.X = 2.0f * mouse.X / ((float)viewport.Width) - 1;
            vec.Y = -(2.0f * mouse.Y / ((float)viewport.Height) - 1);
            vec.Z = 0;
            vec.W = 1;

            EVector4 dir = new EVector4(vec.X, vec.Y, 1, 1);

            EMatrix4 viewInv = EMatrix4.Invert(view);
            EMatrix4 projInv = EMatrix4.Invert(projection);

            EVector4.Transform(ref vec, ref projInv, out vec);
            EVector4.Transform(ref vec, ref viewInv, out vec);

            EVector4.Transform(ref dir, ref projInv, out dir);
            EVector4.Transform(ref dir, ref viewInv, out dir);

            dir.X /= dir.W;
            dir.Y /= dir.W;
            dir.Z /= dir.W;

            ray = new EVector3(new EVector3(dir.X, dir.Y, dir.Z));

            if (vec.W > double.Epsilon || vec.W < double.Epsilon)
            {
                vec.X /= vec.W;
                vec.Y /= vec.W;
                vec.Z /= vec.W;
            }

            return new EVector3(vec.X, vec.Y, vec.Z);
        }

        /// <summary>
        /// Tests specific model, if it is hit
        /// </summary>
        private static IntersectionResult RayModelTest(EVector3 ray, EVector3 pos, EModel model)
        {
            IntersectionResult res = null;

            foreach (var mesh in model.Meshes)
            {
                for (int i = 0; i < mesh.Indices.Length; i += 3)
                {
                    Intersection result = RayTriangleTest(ray, pos,
                            mesh.Vertices[mesh.Indices[i]],
                            mesh.Vertices[mesh.Indices[i + 1]],
                            mesh.Vertices[mesh.Indices[i + 2]]);

                    if (result != null)
                    {
                        res = new IntersectionResult();
                        res.FaceVertices.Add(mesh.Vertices[mesh.Indices[i]]);
                        res.FaceVertices.Add(mesh.Vertices[mesh.Indices[i + 1]]);
                        res.FaceVertices.Add(mesh.Vertices[mesh.Indices[i + 2]]);
                        res.TextureCoords.Add(mesh.TexCoords[mesh.Indices[i]]);
                        res.TextureCoords.Add(mesh.TexCoords[mesh.Indices[i + 1]]);
                        res.TextureCoords.Add(mesh.TexCoords[mesh.Indices[i + 2]]);
                        res.Intersection = result;

                        // return res;
                    }
                }
            }

            return res;
        }


        /// <summary>
        /// Tests specific triangle, if it is hit
        /// </summary>
        private static Intersection RayTriangleTest(EVector3 ray, EVector3 pos, EVector3 p0, EVector3 p1, EVector3 p2)
        {
            //Vector3d E = p0 - p2;
            //Vector3d F = p1 - p2;
            //Vector3d G = pos - p2;

            //  // Compute the vector containing t and the coordinates k and l
            //  Vector3d tkl = 1/Vector3d.Dot(Vector3d.Cross(ray, F), E) *
            //           new Vector3d(Vector3d.Dot(Vector3d.Cross(G, E), F),
            //                     Vector3d.Dot(Vector3d.Cross(ray, F), G),
            //                     Vector3d.Dot(Vector3d.Cross(G, E), ray));

            //  // Check if t and the intersection point (k, l) are acceptable
            //  if (/*tkl.X < tmin[i] &&*/ tkl.Y > 0.0f && tkl.Z > 0.0f && (tkl.Y + tkl.Z) < 1)
            //  {
            //      return new Intersection(tkl.Y, tkl.Z, tkl.X);
            //  }
            //  else return null;


            // find edges
            EVector3 edge1 = p1 - p0;
            EVector3 edge2 = p2 - p0;

            EVector3 planeNormal = EVector3.Cross(edge1, edge2);
            float d = EVector3.Dot((p0 - pos), planeNormal) / EVector3.Dot(ray, planeNormal);
            pos = pos + ray * d;

            float epsilon = 0.000001f;

            EVector3 pvec = EVector3.Cross(ray, edge2);

            float det = EVector3.Dot(edge1, pvec);

            // vector lies in the plane of triangle
            if (det < epsilon)
                return null;

            // calculate distance from the origin
            EVector3 tvec = pos - p0;

            // calculate U
            float u = EVector3.Dot(tvec, pvec);
            if (u < 0.0f || u > det)
                return null;

            // calculate V
            EVector3 qvec = EVector3.Cross(tvec, edge1);
            float v = EVector3.Dot(ray, qvec);
            if (v < 0 || u + v > det)
                return null;

            float t = EVector3.Dot(edge2, qvec);

            // do scaling
            float inv_det = 1.0f / det;
            t *= inv_det;
            u *= inv_det;
            v *= inv_det;

            return new Intersection(u, v, t);
        }
    }
}
